package org.hybridlabs.source.beautifier;

/**
 * Copyright 2008 hybrid labs
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.hunsicker.jalopy.Jalopy;
import de.hunsicker.jalopy.language.antlr.JavaNode;
import de.hunsicker.jalopy.storage.Loggers;

/**
 * Abstract implementation of the Beautifier interface focussing on Java
 * beautification.
 *
 * @author Karsten Klein, hybrid labs
 * @author Karsten Thoms, itemis AG
 *
 */
public abstract class JavaBeautifier implements Beautifier,
		ImportBeautifierJalopyConstants {

	private static final Pattern PATTERN_NEWLINE = Pattern.compile("\\n");

	private static final Logger LOG = LoggerFactory.getLogger(JavaBeautifier.class);

	private String conventionFilePath;

	private boolean conventionFileInitialized = false;

	public String getConventionFilePath() {
		return conventionFilePath;
	}

	public void setConventionFilePath(String conventionFilePath) {
		this.conventionFilePath = conventionFilePath;
		conventionFileInitialized = false;
	}

	protected JavaNode createJavaNode(
			org.hybridlabs.source.beautifier.CharacterSequence sequence,
			File file) {
		initialize();

		JavaNode node = null;
		Jalopy jalopy = initializeJalopy();
		final PrintStream out = System.out;
		try {
			// redirecting System.out to log4j appender
			System.setOut(outToLog4J(out));

			jalopy.setInput(sequence.getString(), file.getAbsolutePath());

			node = jalopy.parse();
		} catch (NoClassDefFoundError e) {
			e.printStackTrace();
		} finally {
			cleanupJalopy();
			System.setOut(out);
		}

		return node;
	}

	private static PrintStream outToLog4J(final PrintStream realPrintStream) {
		return new PrintStream(realPrintStream){
			@Override
			public void print(final String string) {
				if (LOG.isDebugEnabled()) {
					LOG.debug(string);
				}
			}
			@Override
			public void write(int b){}
			@Override
			public void write(byte[] buf, int off, int len) {}
		};
	}

	private URL testUrl(URL url) {
		if (url != null) {
			InputStream inputStream = null;
			try {
				inputStream = url.openStream();
			} catch (IOException e) {
				return null;
			} finally {
				if (inputStream != null) {
					try {
						inputStream.close();
					} catch (IOException e) {
						// ignore
					}
				}
			}
			return url;
		}
		return null;
	}

	private void initialize() {
		initializeConventionFileUrl();
	}

	private void initializeConventionFileUrl() {
		if (conventionFileInitialized) {
			return;
		}
		conventionFileInitialized = true;

		URL url = null;
		if (conventionFilePath != null) {
			url = testUrl(getClass().getResource(conventionFilePath));

			if (url == null) {
				try {
					url = testUrl(new URL("file:" + conventionFilePath));
				} catch (MalformedURLException e) {
					LOG.error("Cannot read convention file from 'file:"
							+ conventionFilePath + "'.", e);
				}
			}
		}

		if (url == null) {
			url = testUrl(getClass().getResource("/default-convention.xml"));
		}

		if (url != null) {
			try {
				Jalopy.setConvention(url);
			} catch (IOException e) {
				LOG.error("Cannot read convention file from '" + url + "'.", e);
			}
		}
	}

	protected int findPositionInCharacterSequence(CharacterSequence sequence,
			int line, int column) {
		Pattern newlinePattern = PATTERN_NEWLINE;
		Matcher newLineMatcher = newlinePattern.matcher(sequence);
		int pos = 0;
		line--;
		while (line > 0) {
			newLineMatcher.find();
			pos = newLineMatcher.end();
			line--;
		}
		pos += column;

		if (pos >= 0) {
			while (pos < sequence.length()
					&& (sequence.charAt(pos) == '\r' || sequence.charAt(pos) == '\n')) {
				pos++;
			}
		}

		return pos;
	}

	protected void format(CharacterSequence sequence, File file) {
		Jalopy jalopy = initializeJalopy();
		try {
			jalopy.setInput(sequence.getString(), file.getAbsolutePath());
			StringBuffer sb = new StringBuffer();
			jalopy.setOutput(sb);
			jalopy.format();
			sequence.set(sb);
		} finally {
			cleanupJalopy();
		}
	}

	private Jalopy initializeJalopy() {
		Jalopy jalopy = new Jalopy();
		jalopy.setInspect(false);
		jalopy.setBackup(false);
		jalopy.setForce(false);
		// NOTE: the convention file is static (done during first initialize()
		// invocation)
		return jalopy;
	}

	// TODO: Factor out to utility class
	// see issue#34
	private Class Log4j_Logger;
	private Method Logger_getAllAppenders;
	private Method Logger_removeAppender;
	private boolean useLog4j = true;
	/**
	 * @see http://code.google.com/p/hybridlabs-beautifier/issues/detail?id=14
	 */
	@SuppressWarnings("unchecked")
	private void cleanupJalopy() {
		if (!useLog4j) return;

		// log4j-over-slf4j does not have the methods getAllAppenders and removeAppender
		try {
			// Object is here in fact of type org.apache.log4j.Appender,
			// but only if library is run with Log4j
			List<Object> toBeDeleted = new ArrayList<Object>();

			Log4j_Logger = Class.forName("org.apache.log4j.Logger");
			Object logger = Log4j_Logger.getField("ALL");
			if (Logger_getAllAppenders == null) {
				Logger_getAllAppenders = logger.getClass().getMethod("getAllAppenders", (Class[])null);
				Logger_removeAppender  = logger.getClass().getMethod("removeAppender", Class.forName("org.apache.log4j.Appender"));
			}
			for (Enumeration<Object> it = (Enumeration<Object>) Logger_getAllAppenders.invoke(logger, (Object[])null); it.hasMoreElements();) {
				Object obj = it.nextElement();
				String name = obj.getClass().getName();
				if (name.equals("de.hunsicker.jalopy.Jalopy$SpyAppender")) {
					toBeDeleted.add((Object) obj);
				}
			}

			for (Object appender : toBeDeleted) {
				Logger_removeAppender.invoke(logger, appender);
			}
		// SecurityException, NoSuchMethodException, IllegalArgumentException, IllegalAccessException, InvocationTargetException
		} catch (Exception e) {
			useLog4j = false; // will only occur with slf4j
			LOG.debug("Log4j not found.");
		}
	}

}
